/*
 * Copyright 2013 Agustina Martinez-Garcia

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
 */
package uk.ljmu.metadata.schemas;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.log4j.Logger;

import uk.ljmu.metadata.errorhandling.MetadataException;
import uk.ljmu.metadata.staticschemas.DoapSchema;
import uk.ljmu.metadata.staticschemas.FedoraRelsExtOntology;
import uk.ljmu.metadata.staticschemas.GeoSchema;
import uk.ljmu.metadata.staticschemas.StaticDDI;
import uk.ljmu.metadata.staticschemas.StaticJenaDublinCore;
import uk.ljmu.metadata.staticschemas.StaticJenaDublinCoreTerms;
import uk.ljmu.metadata.staticschemas.StaticJenaFoaf;
import uk.ljmu.metadata.staticschemas.StaticJenaRDF;
import uk.ljmu.metadata.staticschemas.StaticJenaRDFS;
import uk.ljmu.metadata.staticschemas.StaticQudex;
import uk.ljmu.metadata.staticschemas.StaticSkos;
import uk.ljmu.metadata.utilities.MetadataUtilities;

import com.hp.hpl.jena.rdf.model.Model;
import com.hp.hpl.jena.rdf.model.ModelFactory;
import com.hp.hpl.jena.rdf.model.ResIterator;
import com.hp.hpl.jena.rdf.model.Resource;
import com.hp.hpl.jena.rdf.model.Statement;
import com.hp.hpl.jena.vocabulary.RDF;
import com.hp.hpl.jena.vocabulary.RDFS;

public class SchemaInstanceContainer {
    private Logger log = Logger.getLogger(SchemaInstanceContainer.class);
    private List<SchemaInstance> listSchemaInstances;

    /**
     * @return the listSchemaInstances
     */
    public List<SchemaInstance> getListSchemaInstances() {
	return listSchemaInstances;
    }

    /**
     * @param listSchemaInstances
     *            the listSchemaInstances to set
     */
    public void setListSchemaInstances(List<SchemaInstance> listSchemaInstances) {
	this.listSchemaInstances = listSchemaInstances;
    }

    public SchemaInstanceContainer() {
	setListSchemaInstances(new ArrayList<SchemaInstance>());
    }

    /**
     * 
     * @param ins
     *            generic SchemaInstance
     */
    public void addSchemaInstance(SchemaInstance ins) {
	this.getListSchemaInstances().add(ins);
    }

    /**
     * Creates a list containing all the properties included in a given schema
     * 
     * @param uriSchema
     *            URL of the source schema
     * @throws Exception
     */
    public void readPropertiesFromSchema(String uriSchema) throws Exception {
	SchemaInstance schema = new SchemaInstance();
	List<SchemaElem> elemlist = new ArrayList<SchemaElem>();
	Model activeModel = ModelFactory.createDefaultModel();

	activeModel = ModelFactory.createDefaultModel();

	try {
	    activeModel.read(uriSchema);
	} catch (Exception ex) {
	    if (MetadataUtilities.MDSchemasUrl.containsValue(uriSchema)) {
		String prefix = (String) getKeysByValue(
			MetadataUtilities.MDSchemasUrl, uriSchema);
		if (prefix != null)
		    activeModel = findStaticSchemaModel(prefix);
		else {
		    log.fatal(ex.getMessage());
		    throw ex;
		}
	    }
	    // GO FOR STATIC PROPERTY CHECKING
	    // CHECK SCHEMA NAME
	    /*
	     * if
	     * (uriSchema.equalsIgnoreCase(MetadataUtilities.MDSchemasUrl.get(
	     * "foaf"))) activeModel = DoapSchema.getModel(); else if
	     * (uriSchema.
	     * equalsIgnoreCase(MetadataUtilities.MDSchemasUrl.get("rel")) ||
	     * uriSchema
	     * .equalsIgnoreCase(MetadataUtilities.MDSchemasUrl.get("rel-ext")))
	     * activeModel = FedoraRelsExtOntology.getModel(); else if
	     * (uriSchema
	     * .equalsIgnoreCase(MetadataUtilities.MDSchemasUrl.get("doap")))
	     * activeModel = StaticJenaFoaf.getModel(); else if
	     * (uriSchema.equalsIgnoreCase
	     * (MetadataUtilities.MDSchemasUrl.get("rdfs"))) activeModel =
	     * StaticJenaRDFS.getModel(); else if
	     * (uriSchema.equalsIgnoreCase(MetadataUtilities
	     * .MDSchemasUrl.get("dc"))) activeModel =
	     * StaticJenaDublinCore.getModel(); else if
	     * (uriSchema.equalsIgnoreCase
	     * (MetadataUtilities.MDSchemasUrl.get("rdf"))) activeModel =
	     * StaticJenaRDF.getModel(); else if
	     * (uriSchema.equalsIgnoreCase(MetadataUtilities
	     * .MDSchemasUrl.get("dcterms"))) activeModel =
	     * StaticJenaDublinCoreTerms.getModel(); else if
	     * (uriSchema.equalsIgnoreCase
	     * (MetadataUtilities.MDSchemasUrl.get("geo"))) activeModel =
	     * GeoSchema.getModel(); else if
	     * (uriSchema.equalsIgnoreCase(MetadataUtilities
	     * .MDSchemasUrl.get("qudex"))) activeModel =
	     * StaticQudex.getModel();
	     */
	}
	ResIterator schemaProperties = activeModel.listSubjectsWithProperty(
		RDF.type, RDF.Property);

	while (schemaProperties.hasNext()) {
	    Resource res = (Resource) schemaProperties.next();
	    try {
		SchemaElem aux = new SchemaElem();
		if (res.getProperty(RDFS.label) != null)
		    aux.setLabel(res.getProperty(RDFS.label).getString());
		aux.setType(RDF.Property.getLocalName());
		if (res.getProperty(RDFS.comment) != null) {
		    String comment = res.getProperty(RDFS.comment).getString();
		    aux.setComment(comment);
		}

		elemlist.add(aux);
	    } catch (Exception ex) {
		log.fatal(ex.getMessage());
	    }
	}
	schema.setSchemaElemsList(elemlist);
    }

    /**
     * Returns a SchemaElement from a particular metadata schema
     * 
     * @param activeModel
     *            the Schema model object
     * @param prefix
     *            XML prefix of the given schema
     * @param uriSchema
     *            URL of the source Metadata schema
     * @param elementName
     *            element to search in the schema
     * 
     * @return SchemaElem object containing the information of a particular
     *         element from a given metadata schema
     * 
     * @throws MetadataException
     */
    public SchemaElem getElementFromSchema(Model activeModel, String prefix,
	    String uriSchema, String elementName) throws MetadataException {
	boolean enc = false, dynamic = true;
	SchemaElem aux = new SchemaElem();

	activeModel = ModelFactory.createDefaultModel();

	try {
	    activeModel.read(uriSchema);
	} catch (Exception ex) {
	    // GO FOR STATIC PROPERTY CHECKING
	    // CHECK SCHEMA NAME
	    dynamic = false;
	}

	if (dynamic) {
	    ResIterator schemaProperties = activeModel
		    .listSubjectsWithProperty(RDF.type, RDF.Property);

	    while (schemaProperties.hasNext()) {
		Resource res = (Resource) schemaProperties.next();
		try {
		    if (res.getProperty(RDFS.label) != null
			    && res.getProperty(RDFS.label).getString()
				    .equalsIgnoreCase(elementName)) {
			aux.setLabel(res.getProperty(RDFS.label).getString());
			aux.setType(RDF.Property.getLocalName());

			if (res.getProperty(RDFS.comment) != null) {
			    String comment = res.getProperty(RDFS.comment)
				    .getString();
			    aux.setComment(comment);
			}
			aux.setNamespace(uriSchema);
			enc = true;
			break;
		    }
		} catch (Exception ex) {
		    System.out.println(ex.getMessage());
		}
	    }
	} else {
	   
	    aux = this.findStaticElement(prefix, elementName, uriSchema);
	    if (aux == null)
		enc = false;
	   // boolean elementPresent = false;
	   /* if (prefix.equalsIgnoreCase("foaf"))
		elementPresent = StaticJenaFoaf.containsProperty(elementName);
	    else if (prefix.equalsIgnoreCase("rdfs"))
		elementPresent = StaticJenaRDFS.containsProperty(elementName);
	    else if (prefix.equalsIgnoreCase("dc"))
		elementPresent = StaticJenaDublinCore
			.containsProperty(elementName);
	    else if (prefix.equalsIgnoreCase("rel"))
		elementPresent = FedoraRelsExtOntology
			.containsProperty(elementName);
	    else if (prefix.equalsIgnoreCase("geo"))
		elementPresent = GeoSchema.containsProperty(elementName);
	    else if (prefix.equalsIgnoreCase("doap"))
		elementPresent = DoapSchema.containsProperty(elementName);
	    else if (prefix.equalsIgnoreCase("rdf"))
		elementPresent = StaticJenaRDF.containsProperty(elementName);
	    else if (prefix.equalsIgnoreCase("dcterms"))
		elementPresent = StaticJenaDublinCoreTerms
			.containsProperty(elementName);
	    else if (prefix.equalsIgnoreCase("qudex"))
		elementPresent = StaticQudex.containsProperty(elementName);
	    if (elementPresent) {
		aux.setLabel(elementName);
		aux.setType(RDF.Property.getLocalName());

		aux.setNamespace(uriSchema);
		enc = true;
	    } else
		enc = false;*/
	}
	if (enc)
	    return aux;
	else
	    return null;
    }

    /**
     * @param activeModel
     *            the Metadata Model object
     * @param prefix
     * @param uriSchema
     * @param elementName
     * @return
     * @throws MetadataException
     */
    public SchemaElem getElementFromSchemaByLabel(Model activeModel,
	    String prefix, String uriSchema, String elementName)
	    throws MetadataException {
	boolean dynamic = true;

	activeModel = ModelFactory.createDefaultModel();

	try {
	    activeModel.read(uriSchema);
	} catch (Exception ex) {
	    // GO FOR STATIC PROPERTY CHECKING
	    // CHECK SCHEMA NAME
	    dynamic = false;
	}
	if (dynamic) {
	    ResIterator schemaResources = activeModel
		    .listResourcesWithProperty(RDFS.label);

	    while (schemaResources.hasNext()) {
		Resource res = (Resource) schemaResources.next();

		if (res.getLocalName().equalsIgnoreCase(elementName)) {
		    SchemaElem elem = new SchemaElem();

		    // Common ELEMENTS label, type and comment
		    Statement type = res.getProperty(RDF.type);

		    if (type != null) {
			Resource typeRes = type.getResource();
			elem.setType(typeRes.getLocalName());
		    }

		    String comment = res.getProperty(RDFS.comment) != null ? res
			    .getProperty(RDFS.comment).getString()
			    : "";

		    elem.setComment(comment);
		    elem.setLabel(res.getProperty(RDFS.label).getString());
		    elem.setNamespace(uriSchema);
		    elem.setElementName(res.getLocalName());

		    return elem;
		}
	    }
	} else {
	    SchemaElem elem = findStaticElement(prefix, elementName, uriSchema);
	    return elem;
	}
	// ELEMENT NOT FOUND
	return null;
    }

    /**
     * Given the spreadsheet headers, parses them and generates the model
     * elements from the schema associated which each header, i.e. Dublin Core
     * schema will generate a ResIterator containing the elements in the rdf
     * schema
     * 
     * @param spreadsheetHeaders
     * @return true if operation successful, false otherwise
     */
    public boolean initialiseSchemasInstanceContainer(
	    List<String> spreadsheetHeaders) {
	List<String> parsed = new ArrayList<String>();
	boolean schemaFound = false;
	// long iniTime = 0;
	for (String header : spreadsheetHeaders) {
	    // iniTime = System.currentTimeMillis();
	    String[] prefix = header.split(":");
	    if (prefix.length != 2 || parsed.contains(prefix[0])) {
		// prefix has not have the form prefix:element_name
		continue;
	    } else if (!schemaFound){
		schemaFound = true;
	    }
	    // Find schema and load Resource Iterator
	    boolean loaded = loadResourceIteratorFromSchema(prefix[0]);
	    if (!loaded) {
		log.fatal("Resource Iterator from Model with prefix "
			+ prefix[0] + " not loaded, column ignored");
	    }
	    parsed.add(prefix[0]);
	    // AMG- FEB2011 Remove all the items in the list for the parsed
	    // schema, only need to parse it ONCE
	}
	return schemaFound ? true : false;
    }

    /**
     * Find existing SchemaInstance in the array of SchemaInstances by schemaUri
     * 
     * @param uriSchema
     * @return null if not found, schemaInstace object otherwise
     */
    private SchemaInstance findSchemaInstance(String uriSchema) {
	for (SchemaInstance schema : this.listSchemaInstances) {
	    if (schema.getSchemaURI().equalsIgnoreCase(uriSchema))
		return schema;
	}
	return null;
    }

    /**
     * Loads ResItertor in a given schemainstance object by schema prefix
     * 
     * @param prefix
     * @return false if operation not successful, true otherwise
     */
    private boolean loadResourceIteratorFromSchema(String pre) {
	boolean dynamic = true;

	// Find Schema URI
	String schemaUri = "";
	// TODO FIX THIS HARDCODED VALUE
	if (pre.equalsIgnoreCase("rel"))
	    schemaUri = MetadataUtilities.MDSchemasUrl2.get("rel-ext");
	else
	    schemaUri = MetadataUtilities.MDSchemasUrl2.get(pre);
	if (schemaUri == null) {
	    log
		    .fatal("loadResourceIteratorFromSchema: prefix not valid, schema not found...");
	    return false;
	}
	SchemaInstance schemaInstance = this.findSchemaInstance(schemaUri);

	if (schemaInstance == null) {
	    // Create a New One
	    schemaInstance = new SchemaInstance();
	    schemaInstance.setSchemaPrefix(pre);
	    schemaInstance.setSchemaURI(schemaUri);
	    // Associate Resource Model Iterator
	    Model model = ModelFactory.createDefaultModel();
	    if (MetadataUtilities.dynamicParsingSchemasList.contains(pre)) {
			try {
			    model.read(schemaUri);
			} catch (Exception ex) {
			    log.fatal(ex.getMessage());
			    dynamic = false;
			}
	    } else dynamic = false;
	    
	    if (dynamic) {
		schemaInstance.setModel(model);
	    } else {
		Model staticModel = findStaticSchemaModel(pre);
		if (staticModel != null) {
		    schemaInstance.setModel(staticModel);
		    schemaInstance.setDynamicActive(false);
		} else {
		    // AMG (15/11) The schema prefix was found but we don't have
		    // an associated Model to validate against
		    schemaInstance.setValidationActive(false);
		    schemaInstance.setDynamicActive(false);
		    // log.fatal("Schema with prefix " + prefix +
		    // " not found in list of supported schemas");
		    // return false;
		}
	    }
	    this.getListSchemaInstances().add(schemaInstance);
	    return true;
	} else {
	    // loaded
	    return true;
	}
    }

    /**
     * Returns a schema element object if found in a given metadata schema
     * obtained from the offline version of the schema
     * 
     * @param prefix
     *            metadata schema prefix
     * @param elementName
     *            element to find in the schema
     * @param uriSchema
     *            metadata schema URI
     * @return schemaElem instance if found, null otherwise
     */
    private SchemaElem findStaticElement(String prefix, String elementName,
	    String uriSchema) {
	boolean enc = false;
	if (prefix.equalsIgnoreCase("foaf"))
	    enc = StaticJenaFoaf.containsProperty(elementName);
	else if (prefix.equalsIgnoreCase("rdfs"))
	    enc = StaticJenaRDFS.containsProperty(elementName);
	else if (prefix.equalsIgnoreCase("dc"))
	    enc = StaticJenaDublinCore.containsProperty(elementName);
	else if (prefix.equalsIgnoreCase("rel"))
	    enc = FedoraRelsExtOntology.containsProperty(elementName);
	else if (prefix.equalsIgnoreCase("doap"))
	    enc = DoapSchema.containsProperty(elementName);
	else if (prefix.equalsIgnoreCase("geo"))
	    enc = GeoSchema.containsProperty(elementName);
	else if (prefix.equalsIgnoreCase("rdf"))
	    enc = StaticJenaRDF.containsProperty(elementName);
	else if (prefix.equalsIgnoreCase("dcterms"))
	    enc = StaticJenaDublinCoreTerms.containsProperty(elementName);
	else if (prefix.equalsIgnoreCase("qudex"))
	    enc = StaticQudex.containsProperty(elementName);
	else if (prefix.equalsIgnoreCase("ddi2"))
	    enc = StaticDDI.containsProperty(elementName);
	else if (prefix.equalsIgnoreCase("skos"))
	    enc = StaticSkos.containsProperty(elementName);

	if (enc) {
	    SchemaElem elem = new SchemaElem();
	    // Common ELEMENTS label, type and comment
	    elem.setComment("");
	    elem.setLabel(elementName);
	    elem.setNamespace(uriSchema);
	    elem.setPropertyValueType("");
	    elem.setType(RDF.Property.getLocalName());
	    elem.setElementName(elementName);
	    return elem;
	} else return null; // Element not present in the schema
    }

    /**
     * Returns a RDF Jena Model if found from the given schema prefix the
     * offline version of the schema
     * 
     * @param prefix
     *            metadata schema prefix
     * @return RDF Jena model if found, null otherwise
     */
    public static Model findStaticSchemaModel(String prefix) {

	if (prefix.equalsIgnoreCase("foaf"))
	    return StaticJenaFoaf.getModel();
	else if (prefix.equalsIgnoreCase("rdfs"))
	    return StaticJenaRDFS.getModel();
	else if (prefix.equalsIgnoreCase("dc"))
	    return StaticJenaDublinCore.getModel();
	else if (prefix.equalsIgnoreCase("rel"))
	    return FedoraRelsExtOntology.getModel();
	else if (prefix.equalsIgnoreCase("doap"))
	    return DoapSchema.getModel();
	else if (prefix.equalsIgnoreCase("geo"))
	    return GeoSchema.getModel();
	else if (prefix.equalsIgnoreCase("rdf"))
	    return StaticJenaRDF.getModel();
	else if (prefix.equalsIgnoreCase("dcterms"))
	    return StaticJenaDublinCoreTerms.getModel();
	else if (prefix.equalsIgnoreCase("qudex"))
	    return StaticQudex.getModel();
	else if (prefix.equalsIgnoreCase("ddi2"))
	    return StaticDDI.getModel();
	else if (prefix.equalsIgnoreCase("skos"))
	    return StaticSkos.getModel();

	return null;
    }

    /**
     * Adds a new metadata element to an existing schemaInstance object if the
     * element is present in the given schema
     * 
     * @param prefix
     *            metadata schema prefix
     * @param uriSchema
     *            URI of the metadata schema
     * @param elementName
     *            element string name
     * @return schemaElem instance if element present, null otherwise
     */
    public boolean addMetadataElementToSchemaInstance(String prefix,
	    String uriSchema, String elementName) {
	// Find Schema Instance
	SchemaInstance schemaInstance = findSchemaInstance(uriSchema);

	if (schemaInstance != null && schemaInstance.isValidationActive()) {

	    if (schemaInstance.isDynamicActive()) {
		ResIterator schemaResources = schemaInstance.getModel()
			.listResourcesWithProperty(RDFS.label);

		while (schemaResources.hasNext()) {
		    Resource res = (Resource) schemaResources.next();

		    if (res.getLocalName().equalsIgnoreCase(elementName)) {
			SchemaElem elem = new SchemaElem();
			// Common ELEMENTS label, type and comment
			Statement type = res.getProperty(RDF.type);

			if (type != null) {
			    Resource typeRes = type.getResource();
			    elem.setType(typeRes.getLocalName());
			}
			Statement propertyValueType = res
				.getProperty(RDFS.range);
			if (propertyValueType != null) {
			    Resource propertyValueTypeRes = propertyValueType
				    .getResource();
			    elem.setPropertyValueType(propertyValueTypeRes
				    .getLocalName());
			} else {
			    if (schemaInstance
				    .getSchemaPrefix()
				    .equals(
					    MetadataUtilities.FEDORA_RELATIONS_NS_PREFIX))
				elem
					.setPropertyValueType(MetadataUtilities.propertyTypes.Resource
						.toString());
			    else
				elem.setPropertyValueType("");
			}
			String comment = res.getProperty(RDFS.comment) != null ? res
				.getProperty(RDFS.comment).getString()
				: "";

			elem.setComment(comment);
			elem.setLabel(res.getProperty(RDFS.label).getString());
			elem.setNamespace(uriSchema);
			elem.setElementName(res.getLocalName());

			schemaInstance.getSchemaElemsList().add(elem);
			// AMG- Just one ocurrence, Otherwise get repeated
			break;
		    }
		}
	    }// END isDynamicActive
	    else {
		SchemaElem elem = this.findStaticElement(prefix, elementName, uriSchema);
		if (elem != null)
		    schemaInstance.getSchemaElemsList().add(elem);
		else {
		    log.fatal("Metadata Element not added: " + elementName + " not present in the Schema.");
		    return false;
		}
	    }
	} else if (schemaInstance == null) {
	    log
		    .fatal("Metadata Schema not valid, it is not a supported schema (Prefix: "
			    + prefix + ")");
	    return false;
	} else {
	    // Validation is not active so we just create the element and return
	    // it
	    SchemaElem elem = new SchemaElem();
	    elem.setComment("Element from custom namespace");
	    elem.setLabel(elementName);
	    elem.setNamespace(uriSchema);
	    elem.setElementName(elementName);
	    elem.setType("customElement");
	    elem.setPropertyValueType("element");
	    schemaInstance.getSchemaElemsList().add(elem);

	}
	return true;
    }

    private static <T, E> T getKeysByValue(Map<T, E> map, E value) {
	for (Entry<T, E> entry : map.entrySet()) {
	    if (entry.getValue().equals(value)) {
		return entry.getKey();
	    }
	}
	return null;
    }
    
    public void cleanEmptySchemaInstances() {
	List<Integer> removedElements = new ArrayList<Integer>();
	List<SchemaInstance> aux = this.getListSchemaInstances();
	int i = 0;
	for (SchemaInstance instance : aux) {
	    if (instance.getSchemaElemsList().size() == 0)
		removedElements.add(i);
	    i++;
	}
	for (Integer index : removedElements) {
	    aux.remove(aux.get(index));
	}
	this.setListSchemaInstances(aux);
    }

}
